package edu.uw.cs.cse461.sp12.OS;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import edu.uw.cs.cse461.sp12.OS.RPCCallable.RPCCallableMethod;
import edu.uw.cs.cse461.sp12.OS.exceptions.DDNSAuthorizationException;
import edu.uw.cs.cse461.sp12.OS.exceptions.DDNSNoAddressException;
import edu.uw.cs.cse461.sp12.OS.exceptions.DDNSNoSuchNameException;
import edu.uw.cs.cse461.sp12.OS.exceptions.DDNSRuntimeException;
import edu.uw.cs.cse461.sp12.OS.exceptions.DDNSTTLExpiredException;
import edu.uw.cs.cse461.sp12.OS.exceptions.DDNSZoneException;
import edu.uw.cs.cse461.sp12.util.Base64;
import edu.uw.cs.cse461.sp12.util.DB461.DB461Exception;
import edu.uw.cs.cse461.sp12.util.DB461.RecordSet;
import edu.uw.cs.cse461.sp12.util.SNetDB461;
import edu.uw.cs.cse461.sp12.util.SNetDB461.CommunityRecord;
import edu.uw.cs.cse461.sp12.util.SNetDB461.Photo;
import edu.uw.cs.cse461.sp12.util.SNetDB461.PhotoRecord;

public class SNetController extends RPCCallable {
	private RPCCallableMethod<SNetController> fetchUpdate;
	private RPCCallableMethod<SNetController> fetchPhoto;
	private static final String GENERATION = "generation";
	private static final String MYPHOTOHASH = "myphotohash";
	private static final String CHOSENPHOTOHASH = "chosenphotohash";
	private String systemName = OS.config().getProperty("host.name");
	
	//Constructor for SNetController
	SNetController() throws Exception {
		SNetDB461 db = null;
		try {
		db = new SNetDB461();
		//register our own name
		db.registerMember(systemName);
		//Make sure the community table contains records for the root
		db.registerMember("");
		} finally {
			if (db != null) db.discard();
		}
		
		// Set up the method descriptor variable to refer to this->_fetchUpdate() and fetchPhoto
		fetchUpdate = new RPCCallableMethod<SNetController>(this, "_fetchUpdate");
		fetchPhoto = new RPCCallableMethod<SNetController>(this, "_fetchPhoto");
		// Register the method with the RPC service as externally invocable method "echo"
		((RPCService)OS.getRPCService("rpc")).registerHandler(servicename(), "fetchUpdate", fetchUpdate );
		((RPCService)OS.getRPCService("rpc")).registerHandler(servicename(), "fetchPhoto", fetchPhoto );

	}

	@Override
	public String servicename() {
		return "snet";
	}

	@Override
	public void shutdown() {
		// TODO Auto-generated method stub
		
	}
	
	public String[] getMembers() throws DB461Exception {
		SNetDB461 db = null;
		String[] members = null;
		try {
			db = new SNetDB461();
			RecordSet<CommunityRecord> records = db.COMMUNITYTABLE.readAll();
			members = new String[records.size()];
			for (int i = 0; i < records.size(); i++) {
				members[i] = records.get(i).name;
			}
			
		} finally {
			if (db != null) db.discard();
		}
		
		return members;
	}
	
	public void fixDB() throws DB461Exception {
		SNetDB461 db = null;
		try {
			db = new SNetDB461();
			db.checkAndFixDB(null);
			
		} finally {
			if (db != null) db.discard();
		}
	}
	
	/**
	 * Contact a user:
	 * 		Send them all my community records and photos I need
	 * 		Receive updates to my community members and list of photos to fetch
	 * 		Fetch photos, write to disk, update photo table
	 * @throws DDNSZoneException 
	 * @throws DDNSTTLExpiredException 
	 * @throws DDNSRuntimeException 
	 * @throws DDNSNoSuchNameException 
	 * @throws DDNSAuthorizationException 
	 * @throws DDNSNoAddressException 
	 * @throws IOException 
	 * @throws DB461Exception 
	 * @throws JSONException 
	 */
	public void contactMember(String memberName) throws DDNSNoAddressException, 
		DDNSAuthorizationException, DDNSNoSuchNameException, DDNSRuntimeException, 
		DDNSTTLExpiredException, DDNSZoneException, IOException, DB461Exception, JSONException {
		
		// Resolve DDNS record from memberName
		DDNSRRecord record = 
				((DDNSResolverService)OS.getRPCService("ddnsresolver")).resolve(memberName);
		
		// Prepare arguments {community:{MemberField,...}, needphotos:[int, ...]}
		// MemberField ::= membername: {generation: int, myphotohash: int, chosenphotohash:int }
		JSONObject args = new JSONObject();
		JSONObject community = null;
		JSONArray needphotos = null;
		SNetDB461 db = null;
		try {
		   db = new SNetDB461();
		   community = cRecVecToJSON(db.COMMUNITYTABLE.readAll());
		   needphotos = getPhotosToFind(db.PHOTOTABLE.readAll());
		} finally {
		   if ( db != null ) db.discard();
		}
		args.put("community", community);
		args.put("needphotos", needphotos);
		
		// Open RPCCallerSocket and send command
		RPCCallerSocket socket = new RPCCallerSocket(record.name, record.ip, Integer.toString(record.port));
		JSONObject response = socket.invoke("snet", "fetchUpdate", args);
		socket.close();
		
		if (response == null)
			throw new IOException("Null response");
		
		// Update community records using response
		JSONObject communityupdates = response.getJSONObject("communityupdates");
		JSONArray names = communityupdates.names();
		try {
		   db = new SNetDB461();
		   for (int i = 0; i < names.length(); i++) {
				String name = names.getString(i);
				CommunityRecord cRec = db.COMMUNITYTABLE.readOne(name);
				cRec.generation = communityupdates.getJSONObject(name).getInt("generation");
				cRec.myPhotoHash = communityupdates.getJSONObject(name).getInt("myphotohash");
				cRec.chosenPhotoHash = communityupdates.getJSONObject(name).getInt("chosenphotohash");
				db.COMMUNITYTABLE.write(cRec);
			}
		} finally {
		   if ( db != null ) db.discard();
		}
		
		// Fetch photos from callee, write to disk, update photo table
		JSONArray photoupdates = response.getJSONArray("photoupdates");
		socket = new RPCCallerSocket(record.name, record.ip, Integer.toString(record.port));
		
		for (int i = 0; i < photoupdates.length(); i++) {
			args = new JSONObject();
			args.put("photohash", photoupdates.get(i));
			response = socket.invoke("snet", "fetchPhoto", args);
			
			// Write file to location in config file
			byte[] photo = Base64.decode(response.getString("photodata"));
			String galleryDir = OS.config().getProperty("snet.config");
			int hash = response.getInt("photohash");
			FileOutputStream fos = new FileOutputStream(galleryDir + Integer.toString(hash));
			fos.write(photo);
			
			// Lookup photo record, update File field, rewrite it to table
			try {
			   db = new SNetDB461();
			   PhotoRecord pRec = db.PHOTOTABLE.readOne(hash);
			   pRec.file = new File(galleryDir + Integer.toString(hash));
			   db.PHOTOTABLE.write(pRec);
			} finally {
			   if ( db != null ) db.discard();
			}
		}
		
		socket.close();
	}
	
	/*
	 * Convert a RecordSet<CommunityRecord> to {community:{MemberField,...}, needphotos:[int, ...]}
	 */
	private JSONObject cRecVecToJSON(RecordSet<CommunityRecord> cRecVec) throws JSONException {
		JSONObject community = new JSONObject();
		for (CommunityRecord rec : cRecVec) {
			JSONObject recObj = new JSONObject();
			recObj.put("generation", rec.generation);
			recObj.put("myphotohash", rec.myPhotoHash);
			recObj.put("chosenphotohash", rec.chosenPhotoHash);
			community.put(rec.name, recObj);
		}
		
		return community;
	}
	
	/*
	 * Finds all PhotoRecords for which no file exists
	 */
	private JSONArray getPhotosToFind(RecordSet<PhotoRecord> pRecVec) throws DB461Exception {
		JSONArray toFind = new JSONArray();
		
		// Loop through all PhotoRecords and add to array if no file exists
		for (PhotoRecord pRec : pRecVec) {
			if (pRec.file == null)
				toFind.put(pRec.hash);
		}
		
		return toFind;
	}
	
	/**
	 * attempt to change a member in the community table to being a friend.
	 * @param friend to attempt to change
	 * @return true if something was changed, false otherwise
	 * @throws DB461Exception
	 */
	public boolean addFriend(String friend) throws DB461Exception {
		SNetDB461 db = null;
		try {
			db = new SNetDB461();
			CommunityRecord r = db.COMMUNITYTABLE.readOne(friend);
			if (r != null && r.isFriend == false) {
				r.isFriend = true;
				db.COMMUNITYTABLE.write(r); //not sure if this is necessary but it can't hurt. i think it is
				return true;
			}
		} finally {
			if (db != null) db.discard();
		}
		return false;
	}
	
	/**
	 * Sets this user's My Photo
	 * @throws DB461Exception 
	 * @throws IOException 
	 * @throws FileNotFoundException 
	 */
	public void setMyPhoto(File file) throws DB461Exception, FileNotFoundException, IOException {
		SNetDB461 db = null;
		Photo newPhoto = new Photo(file);
		try {
		   db = new SNetDB461();
		   CommunityRecord myRecord = db.COMMUNITYTABLE.readOne(systemName);
		   
		   // Decrement old photo's reference count, rewrite or delete it
		   int oldPhotoHash = myRecord.myPhotoHash;
		   PhotoRecord oldPhotoRecord = db.PHOTOTABLE.readOne(oldPhotoHash);
		   oldPhotoRecord.refCount--;
		   if (oldPhotoRecord.refCount == 0){
			   db.PHOTOTABLE.delete(oldPhotoHash);
			   // oldPhotoRecord.file.delete();
		   } else {
			   db.PHOTOTABLE.write(oldPhotoRecord);
		   }
		   
		   // Create new photo record and write it to table
		   PhotoRecord newPhotoRecord = db.createPhotoRecord();
		   newPhotoRecord.file = newPhoto.file();
		   newPhotoRecord.hash = newPhoto.hash();
		   db.PHOTOTABLE.write(newPhotoRecord);
		   
		   // Update my community record and write to table
		   myRecord.myPhotoHash = newPhoto.hash();
		   myRecord.generation++;
		   db.COMMUNITYTABLE.write(myRecord);
		} finally {
		   if ( db != null ) db.discard();
		}
	}
	
	/**
	 * Sets this user's Chosen Photo
	 * @throws DB461Exception 
	 * @throws IOException 
	 * @throws FileNotFoundException 
	 */
	public void setChosenPhoto(File file) throws DB461Exception, FileNotFoundException, IOException {
		SNetDB461 db = null;
		Photo newPhoto = new Photo(file);
		try {
		   db = new SNetDB461();
		   CommunityRecord myRecord = db.COMMUNITYTABLE.readOne(systemName);
		   
		   // Decrement old chosen photo's reference count, rewrite or delete it
		   int oldPhotoHash = myRecord.chosenPhotoHash;
		   PhotoRecord oldPhotoRecord = db.PHOTOTABLE.readOne(oldPhotoHash);
		   oldPhotoRecord.refCount--;
		   if (oldPhotoRecord.refCount == 0){
			   db.PHOTOTABLE.delete(oldPhotoHash);
			   // oldPhotoRecord.file.delete();
		   } else {
			   db.PHOTOTABLE.write(oldPhotoRecord);
		   }
		   
		   // Create new photo record and write it to table
		   PhotoRecord newPhotoRecord = db.createPhotoRecord();
		   newPhotoRecord.file = newPhoto.file();
		   newPhotoRecord.hash = newPhoto.hash();
		   db.PHOTOTABLE.write(newPhotoRecord);
		   
		   // Update my community record and write to table
		   myRecord.chosenPhotoHash = newPhoto.hash();
		   myRecord.generation++;
		   db.COMMUNITYTABLE.write(myRecord);
		} finally {
		   if ( db != null ) db.discard();
		}
	}
	
	/**
	 * Returns users My Photo File
	 * @throws DB461Exception 
	 */
	public File getMyPhoto() throws DB461Exception {
		File myPhoto = null;
		SNetDB461 db = null;
		try {
		   db = new SNetDB461();
		   int hash = db.COMMUNITYTABLE.readOne(systemName).myPhotoHash;
		   myPhoto = db.PHOTOTABLE.readOne(hash).file;
		} finally {
		   if ( db != null ) db.discard();
		}
		return myPhoto;
	}
	
	/**
	 * Returns users My Photo File
	 * @throws DB461Exception 
	 */
	public File getChosenPhoto() throws DB461Exception {
		File chosenPhoto = null;
		SNetDB461 db = null;
		try {
		   db = new SNetDB461();
		   int hash = db.COMMUNITYTABLE.readOne(systemName).chosenPhotoHash;
		   chosenPhoto = db.PHOTOTABLE.readOne(hash).file;
		} finally {
		   if ( db != null ) db.discard();
		}
		return chosenPhoto;
	}
	
	/**
	 * attempt to change a member in the community table to being not a friend.
	 * @param friend to attempt to change
	 * @return true if something was changed, false otherwise
	 * @throws DB461Exception
	 */
	public boolean removeFriend(String friend) throws DB461Exception {
		SNetDB461 db = null;
		try {
			db = new SNetDB461();
			CommunityRecord r = db.COMMUNITYTABLE.readOne(friend);
			if (r != null && r.isFriend == true) {
				r.isFriend = false;
				db.COMMUNITYTABLE.write(r); //not sure if this is necessary but it can't hurt. i think it is
				return true;
			}
		} finally {
			if (db != null) db.discard();
		}
		return false;
	}
	
	
	
	//RPC fetchupdate call when another user calls fetchupdate on our SNet controller
	public JSONObject _fetchUpdate(JSONObject args) throws JSONException, IOException, DB461Exception {
		//create two new json objects to return
		JSONObject result = new JSONObject();
		JSONObject communityUpdates = new JSONObject();
		JSONArray photoUpdates = new JSONArray();
		//open the database
		SNetDB461 db = null;
		try {
		   db = new SNetDB461();
		   //RecordSet<CommunityRecord> cRecVec = db.COMMUNITYTABLE.readAll();
		   JSONObject community = args.getJSONObject("community");
		   JSONArray names = community.names();
		   //loop over all community records given to the method in the args. Check this with what we have, and if we have
		   //a newer record then update it and send it back.
		   for (int i = 0; i < names.length(); i++) {
			   String name = names.getString(i);
			   JSONObject memberRecord = community.getJSONObject(name);
			   CommunityRecord r = db.COMMUNITYTABLE.readOne(name);
			   if (r != null) {
				   //if what we have is newer than what they sent, we must return our new values
				   if (r.generation > memberRecord.getInt(GENERATION)) {
					   JSONObject newMemberRecord = new JSONObject();
					   newMemberRecord.put(GENERATION, r.generation);
					   newMemberRecord.put(MYPHOTOHASH, r.myPhotoHash);
					   newMemberRecord.put(CHOSENPHOTOHASH, r.chosenPhotoHash);
					   communityUpdates.put(name, newMemberRecord);
					   PhotoRecord pr = db.PHOTOTABLE.readOne(r.myPhotoHash);
					   if (pr != null && pr.file != null) { //this could be wrong. the writeup says the file is a string value whereas pr is a File but i didn't see one
						   photoUpdates.put(pr.hash);
					   }
					   
				   }
				   
			   }
		   }
		   //now we must check to see if we have any photos that they requested
		   JSONArray needPhotos = args.getJSONArray("needphotos");
		   for (int i = 0; i < needPhotos.length(); i++) {
			   int hash = needPhotos.getInt(i);
			   PhotoRecord pr = db.PHOTOTABLE.readOne(hash);
			   if (pr != null && pr.file != null) { //this could be wrong. the writeup says the file is a string value whereas pr is a File but i didn't see one
				   photoUpdates.put(pr.hash);
			   }
		   }
		   
		} finally {
		   if ( db != null ) db.discard();
		}
		result.put("communityupdates", communityUpdates);
		result.put("photoupdates", photoUpdates);
		return result;
	
	}
	
	
	//TODO add some return exception if the file isnt found
	public JSONObject _fetchPhoto(JSONObject args) throws JSONException, IOException, DB461Exception {
		int photohash = args.getInt("photohash");
		JSONObject result = new JSONObject();
		SNetDB461 db = null;
		try {
		   db = new SNetDB461();
		   PhotoRecord pr = db.PHOTOTABLE.readOne(photohash);
			if (pr != null && pr.file != null) {
				result.put("photohash", photohash);
				result.put("photodata", Base64.encodeFromFile(pr.file.getAbsolutePath())); //not sure if getabsolutepath is what we want here
				//TODO add some return exception if the file isnt found
			}
		} finally {
		   if ( db != null ) db.discard();
		}
		
		
		return result;
	
	}

}
